Skip to content

perf(callgrind): cut redundant debug-info address lookups on the dump path - #33

Open
codspeed-hq[bot] wants to merge 1 commit into
masterfrom
codspeed-optim-cut-redundant-debug-info-address-lookups-on-callgr-1785563873548
Open

perf(callgrind): cut redundant debug-info address lookups on the dump path#33
codspeed-hq[bot] wants to merge 1 commit into
masterfrom
codspeed-optim-cut-redundant-debug-info-address-lookups-on-callgr-1785563873548

Conversation

@codspeed-hq

@codspeed-hq codspeed-hq Bot commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Writing the profile at exit is a significant part of a Callgrind run: on the python3 testdata/test.py workload the dump phase queries file/line information for ~141k instruction addresses. Almost all of that work is redundant — consecutive addresses belong to the same source line and the same source file.

Three changes remove that redundancy:

1. One-entry range cache for search_all_loctabs() (coregrind/m_debuginfo/debuginfo.c)

Every VG_(get_filename_linenum) / VG_(get_inline_fnname) call walks the whole DebugInfo list and then binary-searches that object's loctab. Since one source line covers several instructions, consecutive queries land in the same loctab entry (or the next one). The cache remembers the last hit and answers those queries with a couple of compares.

It is only reused for the epoch it was found in, and it is dropped in advance_current_DiEpoch() — i.e. whenever debug info is loaded or discarded — so a stale DebugInfo can never be read.

2. Skip the loctab search in VG_(get_inline_fnname) when inline info was never read

Callgrind defaults to --read-inline-info=no, in which case no inltab is ever built and the answer is always "no inline function" — but the search was still performed for every dumped address. The guard mirrors the one already present in its sibling VG_(new_IIPC).

3. Reuse the last file_node in get_debug_pos() (callgrind/dump.c)

The existing debug cache is keyed by instruction address, so it never hits while a dump walks the (all distinct) addresses of a basic block. Every miss called CLG_(get_file_node), which rebuilds the absolute source path on the stack and hashes it character by character. The memo compares the interned (dir, file) pointers returned by the debug info and reuses the previous node; it is reset by init_debug_cache() at the start of each dump, and no guest code runs while dumping, so no debug info can change in between.

Correctness

Verified locally (amd64-linux, --enable-only64bit, Capstone enabled):

  • Callgrind output is byte-for-byte identical to a baseline build of the same tree, on /bin/echo hello with --read-inline-info=yes (exercising the cfni= inline path) and on /bin/ls /etc with --separate-callers=3 — the full callgrind.out including all fl=/fi=/fn= numbering and event counts. Both builds were produced from the same directory and run with ASLR disabled and an identical environment, so the comparison is exact.
  • All 23 Callgrind regression tests pass (0 stderr/stdout/post failures).
  • All 292 Memcheck regression tests pass — a direct check of the core change, since Memcheck's error messages resolve source file and line through search_all_loctabs.
  • make check completes successfully.

Measurement

Measured with CodSpeed walltime runs in a sandbox (x86_64, not a macro runner). Both builds were placed on the same filesystem and interleaved inside a single CodSpeed run (12 rounds each, 1s warmup), so base and head were measured under identical machine conditions — a first attempt using two sequential runs was unusable because the whole machine drifted ~2-3% between them.

Benchmark base head delta
echo, no-inline 229.9 ms 224.0 ms -2.6%
echo, inline 304.2 ms 303.1 ms -0.4%
echo, full-no-inline 244.4 ms 241.4 ms -1.2%
echo, full-with-inline 325.6 ms 324.5 ms -0.3%
python3 test.py, no-inline 1.371 s 1.331 s -2.9%
python3 test.py, inline 1.552 s 1.492 s -3.9%
python3 test.py, full-no-inline 2.051 s 2.030 s -1.0%
python3 test.py, full-with-inline 2.223 s 2.192 s -1.4%

All 8 configurations improve; the win is largest where the dump is a meaningful share of the run and where the --read-inline-info=no early return applies. Execution-dominated workloads are unaffected, as expected. The definitive numbers will come from the repository's own CodSpeed job on the macro runner.

Interleaved A/B run: https://app.codspeed.io/CodSpeedHQ/valgrind-codspeed/runs/6a6db8f71be703b08d0d17ba

… path

Writing the profile at exit queries file/line information for every
instruction address of every dumped basic block (~141k addresses for
`python3 testdata/test.py`). Almost all of that work is redundant:
consecutive addresses belong to the same source line and the same
source file.

- Add a one-entry range cache to search_all_loctabs(). It answers a
  query that lands in the last hit loctab entry (or the next one) with
  a couple of compares instead of walking every DebugInfo and binary
  searching its loctab. It is only reused within the epoch it was found
  in and is dropped by advance_current_DiEpoch(), so a stale DebugInfo
  can never be read.

- Return early from VG_(get_inline_fnname) when --read-inline-info=no.
  No inltab is built in that case, so the answer is always "no inlined
  function", but the loctab search was still performed for every dumped
  address. This mirrors the guard already present in VG_(new_IIPC).

- Memoize the last file_node in get_debug_pos(). The existing debug
  cache is keyed by instruction address so it never hits while walking
  the distinct addresses of a basic block; every miss called
  CLG_(get_file_node), which rebuilds and rehashes the absolute source
  path. The memo compares the interned (dir, file) pointers and is
  reset by init_debug_cache() at the start of each dump.

Callgrind output is byte-for-byte identical to the previous build.
@codspeed-hq

codspeed-hq Bot commented Aug 1, 2026

Copy link
Copy Markdown
Author

Merging this PR will not alter performance

✅ 84 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing codspeed-optim-cut-redundant-debug-info-address-lookups-on-callgr-1785563873548 (2ed8e5b) with master (ae6bf15)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@codspeed-hq
codspeed-hq Bot requested a review from not-matthias August 1, 2026 09:47
@codspeed-hq
codspeed-hq Bot marked this pull request as ready for review August 1, 2026 09:49
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

The PR reduces Callgrind dump overhead by caching adjacent debug-location lookups, skipping inline lookup when inline information is disabled, and reusing the most recently resolved source-file node.

  • Adds an epoch-scoped one-entry loctab cache with invalidation on debug-info lifecycle changes.
  • Avoids loctab searches for inline names when inline information was not loaded.
  • Adds a per-dump (object, directory, filename) memo for Callgrind file nodes.

Confidence Score: 5/5

The PR appears safe to merge, with the new caches preserving the existing debug-location and file-node lookup semantics.

Debug-info cache reuse is constrained by epoch invalidation and canonical loctab ranges, while the Callgrind memo uses conservative pointer identity and is reset for every dump; no concrete blocking or non-blocking defect remains.

Important Files Changed

Filename Overview
coregrind/m_debuginfo/debuginfo.c Adds an epoch-invalidated adjacent-entry loctab cache and bypasses inline-name resolution when inline data was not read; no actionable correctness issue was found.
callgrind/dump.c Adds a per-dump file-node memo keyed by object and interned source-path pointers; the key and reset boundaries preserve existing attribution behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Address lookup] --> B{Inline lookup disabled?}
    B -->|Yes| C[Return no inline name]
    B -->|No or file/line lookup| D{Current epoch and cached loctab entry covers address?}
    D -->|Yes| E[Reuse DebugInfo and location]
    D -->|Next entry covers| F[Advance cached location]
    D -->|No| G[Walk DebugInfos and search loctab]
    G --> H[Refresh epoch-scoped cache]
    E --> I{Same object and interned dir/file?}
    F --> I
    H --> I
    I -->|Yes| J[Reuse file_node]
    I -->|No| K[Resolve and cache file_node]
Loading

Reviews (1): Last reviewed commit: "perf(callgrind): cut redundant debug-inf..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant